﻿#region Copyright Notice
// 
// 
//  
//  Copyright (c) 2009-2011 Anindya Chatterjee
//  
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//  
//      http://www.apache.org/licenses/LICENSE-2.0
//  
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// 
// 
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using AbstractClass.NPerf.Framework;

namespace AbstractClass.NPerf
{
    /// <summary>
    /// Represents the main performance test runner engine.
    /// </summary>
    public class NPerfTestRunner
    {
        private readonly List<object> _classInstances = new List<object>();

        private readonly List<Type> _classTypes = new List<Type>();

        /// <summary>
        /// The list of all completed test units.
        /// </summary>
        public List<TestUnit> CompletedTests = new List<TestUnit>();

        /// <summary>
        /// The result writer which will write the test results.
        /// </summary>
        public ResultTracer ResultWriter;

        /// <summary>
        /// Occurs when all the tests in a test suite get ended.
        /// </summary>
        public EventHandler<TestSuiteRunEventArgs> TestSuiteEnd;

        /// <summary>
        /// Occurs before start of the first test in a test suite.
        /// </summary>
        public EventHandler<TestSuiteRunEventArgs> TestSuiteStart;

        /// <summary>
        /// Occurs when a test unit gets ended.
        /// </summary>
        public EventHandler<TestUnitRunEventArgs> TestUnitEnd;

        /// <summary>
        /// Occurs when a test unit gets failed.
        /// </summary>
        public EventHandler<TestFailedEventArgs> TestUnitFailed;

        /// <summary>
        /// Occurs when a test unit gets ignored.
        /// </summary>
        public EventHandler<TestUnitRunEventArgs> TestUnitIgnored;

        /// <summary>
        /// Occurs before a test unit gets started.
        /// </summary>
        public EventHandler<TestUnitRunEventArgs> TestUnitStart;

        private bool _isAsync;
        private DateTime _startTime;

        /// <summary>
        /// Gets a value indicating whether this <see cref="NPerfTestRunner"/> is running.
        /// </summary>
        /// <value><c>true</c> if running; otherwise, <c>false</c>.</value>
        public bool Running { get; private set; }


        /// <summary>
        /// Gets the list of running test units.
        /// </summary>
        /// <value>The list of running test units.</value>
        public List<TestUnit> RunningTestUnits { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether all the tests have finished.
        /// </summary>
        /// <value><c>true</c> if all tests have finished; otherwise, <c>false</c>.</value>
        public bool TestsFinished { get; private set; }

        /// <summary>
        /// Represents tests total execution time.
        /// </summary>
        public TimeSpan TotalTime { get; private set; }


        /// <summary>
        /// Raises the <see cref="TestSuiteStart"/> event.
        /// </summary>
        /// <param name="e">The <see cref="TestSuiteRunEventArgs"/> instance containing the event data.</param>
        protected void OnTestSuiteStart(TestSuiteRunEventArgs e)
        {
            EventHandler<TestSuiteRunEventArgs> tempEvent = TestSuiteStart;
            if (tempEvent != null)
                TestSuiteStart.Invoke(this, e);
        }

        /// <summary>
        /// Raises the <see cref="TestSuiteEnd"/> event.
        /// </summary>
        /// <param name="e">The <see cref="TestSuiteRunEventArgs"/> instance containing the event data.</param>
        protected void OnTestSuiteEnd(TestSuiteRunEventArgs e)
        {
            EventHandler<TestSuiteRunEventArgs> tempEvent = TestSuiteEnd;
            if (tempEvent != null)
                TestSuiteEnd.Invoke(this, e);
        }

        /// <summary>
        /// Raises the <see cref="TestUnitStart"/> event.
        /// </summary>
        /// <param name="e">The <see cref="TestUnitRunEventArgs"/> instance containing the event data.</param>
        protected void OnTestUnitStart(TestUnitRunEventArgs e)
        {
            EventHandler<TestUnitRunEventArgs> tempEvent = TestUnitStart;
            if (tempEvent != null)
                TestUnitStart.Invoke(this, e);
        }

        /// <summary>
        /// Raises the <see cref="TestUnitEnd"/> event.
        /// </summary>
        /// <param name="e">The <see cref="TestUnitRunEventArgs"/> instance containing the event data.</param>
        protected void OnTestUnitEnd(TestUnitRunEventArgs e)
        {
            EventHandler<TestUnitRunEventArgs> tempEvent = TestUnitEnd;
            if (tempEvent != null)
                TestUnitEnd.Invoke(this, e);
        }

        /// <summary>
        /// Raises the <see cref="TestUnitIgnored"/> event.
        /// </summary>
        /// <param name="e">The <see cref="TestUnitRunEventArgs"/> instance containing the event data.</param>
        protected void OnTestUnitIgnored(TestUnitRunEventArgs e)
        {
            e.TestUnit.TestSuite.IgnoredTests.Add(e.TestUnit);
            EventHandler<TestUnitRunEventArgs> tempEvent = TestUnitIgnored;
            if (tempEvent != null)
                TestUnitIgnored.Invoke(this, e);
        }

        /// <summary>
        /// Raises the <see cref="TestUnitFailed"/> event.
        /// </summary>
        /// <param name="e">The <see cref="TestFailedEventArgs"/> instance containing the event data.</param>
        protected void OnTestUnitFailed(TestFailedEventArgs e)
        {
            EventHandler<TestFailedEventArgs> tempEvent = TestUnitFailed;
            if (tempEvent != null)
                TestUnitFailed.Invoke(this, e);
        }


        /// <summary>
        /// Creates object instance, runs its method and places it in array cache for further invoking.
        /// </summary>
        /// <param name="type">Reflection type of test fixture class to create.</param>
        /// <param name="methodInfo">Reflection method of test to invoke.</param>
        internal void RunMethod(Type type, MethodInfo methodInfo)
        {
            if (type != null)
            {
                //var call = methodInfo.CallMethod<object>();
                //var invoker = methodInfo.GetMethodInvoker();
                int index = _classTypes.IndexOf(type);
                if (index == -1)
                {
                    _classTypes.Add(type);
                    object o = Activator.CreateInstance(type);
                    _classInstances.Add(o);
                    //var d = (Action)Delegate.CreateDelegate(typeof (Action), o, methodInfo, true);
                    //if (d != null)
                    //{
                    //    _testMethods.Add(methodInfo.Name, d);
                    //    d();
                    //}
                    methodInfo.Invoke(o, null);
                    //call(o, null);
                    //invoker(o, null);
                }
                else
                {
                    //invoker(_classInstances[index], null);
                    //call(_classInstances[index], null);
                    methodInfo.Invoke(_classInstances[index], null);
                    //var dTemp = _testMethods[methodInfo.Name] ??
                    //             (Action)Delegate.CreateDelegate(typeof(Action), _classInstances[index], methodInfo, true);
                    //if (dTemp != null) dTemp();
                }
            }
        }


        /// <summary>
        /// Runs the specified test fixture - optionally - in asynchronous mode.
        /// </summary>
        /// <param name="testFixture">The test fixture to run.</param>
        /// <param name="isAsync">if set to <c>true</c> the test will run asynchronously.</param>
        public void RunTestFixture(TestFixture testFixture, bool isAsync)
        {
            TotalTime = TimeSpan.Zero;
            _startTime = DateTime.Now;

            CompletedTests.Clear();
            TestsFinished = false;
            Running = true;

            RunningTestUnits = testFixture.Units;
            var perfTestSuite = new TestSuite
                                    {
                                        TestFixture = testFixture,
                                        Machine = PerfMachine.GetCurrent(),
                                        Os = PerfOs.GetCurrent(),
                                        TimeStamp = DateTime.Now
                                    };

            _isAsync = isAsync;

            foreach (TestUnit unit in testFixture.Units)
            {
                perfTestSuite.Tests.Add(unit);
                unit.TestSuite = perfTestSuite;
            }

            OnTestSuiteStart(new TestSuiteRunEventArgs(perfTestSuite));

            RunTestSuite(perfTestSuite);
        }

        private void RunTestSuite(TestSuite testSuite)
        {
            var t = new Thread(ThreadTestsRun) {Name = "NPerfTestThread"};
            if (_isAsync)
            {
                var data = new TestThreadData {ResetEvent = null, TestSuite = testSuite};
                t.Start(data);
            }
            else
            {
                var resetEvent = new ManualResetEvent(false);
                var data = new TestThreadData {ResetEvent = resetEvent, TestSuite = testSuite};
                t.Start(data);
                resetEvent.WaitOne();
            }
        }

        internal void ThreadTestsRun(object testData)
        {
            var data = (TestThreadData) testData;
            ManualResetEvent resetEvent = data.ResetEvent;
            TestSuite testSuite = data.TestSuite;

            TestFixture tf = testSuite.TestFixture;

            try
            {
                RunMethod(tf.StartUpObjectType, tf.StartUpObjectMethod);
            }
                // ReSharper disable EmptyGeneralCatchClause
            catch (Exception)
                // ReSharper restore EmptyGeneralCatchClause
            {
            }
            foreach (TestUnit tu in testSuite.Tests)
            {
                OnTestUnitStart(new TestUnitRunEventArgs(tu));

                tu.TestResult = new TestResult();
                if (tf != tu.Fixture)
                {
                    try
                    {
                        if (tf != null)
                        {
                            RunMethod(tf.TearDownObjectType, tf.TearDownObjectMethod);
                            if (tu.Fixture != null)
                                RunMethod(tu.Fixture.StartUpObjectType, tu.Fixture.StartUpObjectMethod);
                        }
                    }
                        // ReSharper disable EmptyGeneralCatchClause
                    catch (Exception)
                        // ReSharper restore EmptyGeneralCatchClause
                    {
                    }
                }
                tf = tu.Fixture;
                if (tu.IsIgnored)
                    OnTestUnitIgnored(new TestUnitRunEventArgs(tu));
                else
                {
                    try
                    {
                        var tr = new ThreadRun[tu.Threads];
                        for (int i = 0; i < tu.Threads; i++)
                        {
                            tr[i] = new ThreadRun(this, tu);
                            tr[i].Thread = new Thread(tr[i].ThreadTest) {Name = "NPerfThread_" + i};
                        }
                        for (int i = 0; i < tu.Threads; i++)
                        {
                            tr[i].Thread.Start();
                        }
                        for (int i = 0; i < tu.Threads; i++)
                        {
                            tr[i].Thread.Join();
                        }
                        for (int i = 0; i < tu.Threads; i++)
                        {
                            if (tr[i].ExceptionFound != null)
                            {
                                throw tr[i].ExceptionFound;
                            }
                        }
                        foreach (TestType testKind in tu.TestTypes)
                        {
                            switch (testKind)
                            {
                                case TestType.None:
                                    if (tu.IsGenericCounterUnit)
                                    {
                                        double counterData = 0;
                                        bool shorted = false;
                                        for (int i = 0; i < tu.Threads; i++)
                                        {
                                            counterData += tr[i].CounterSample;
                                            if (tr[i].ShortTime)
                                                shorted = true;
                                        }
                                        tu.TestResult.IsGenericCounterTest = true;
                                        if (!shorted)
                                        {
                                            double avgCounterValue = counterData / (tu.Threads * tu.TestIteration);
                                            tu.TestResult.CategoryName = tu.CategoryName;
                                            tu.TestResult.CounterName = tu.CounterName;
                                            tu.TestResult.InstanceName = tu.InstanceName;
                                            tu.TestResult.CounterValue = avgCounterValue;
                                        }
                                        else
                                        {
                                            tu.TestResult.CounterValue = -1;
                                        }
                                    }
                                    break;
                                case TestType.MemoryUsage:
                                    long totalMemoryUsage = 0;
                                    for (int i = 0; i < tu.Threads; i++)
                                    {
                                        totalMemoryUsage += tr[i].MemoryUsage;
                                    }
                                    long totalMemoryUsageZ = totalMemoryUsage / (tu.Threads * tu.TestIteration);
                                    tu.TestResult.MemoryUsage = totalMemoryUsageZ;
                                    break;
                                case TestType.Duration:
                                case TestType.HitCount:
                                    {
                                        double totalDuration = 0;
                                        for (int i = 0; i < tu.Threads; i++)
                                        {
                                            totalDuration += tr[i].Duration.Sum();
                                        }
                                        var totalDurationZ =
                                            (long) (totalDuration / (tu.Threads * tu.TestIteration) * 1000000000.0);
                                        if (testKind == TestType.Duration)
                                        {
                                            double td = totalDurationZ;
                                            TimePeriod timePeriod;
                                            NormalizeTime(ref td, out timePeriod);
                                            tu.TestResult.Duration = td;
                                            tu.TestResult.DurationTimePeriod = timePeriod;
                                        }
                                        else
                                        {
                                            double td = totalDurationZ;
                                            tu.TimeUnit = TimePeriod.Minute;
                                            NormalizeHitcount(ref td, tu.TimeUnit);
                                            tu.TestResult.HitCount = (int) td;
                                            tu.TestResult.HitCountTimeUnit = tu.TimeUnit;
                                        }
                                    }
                                    break;
                                case TestType.CPUUsage:
                                    {
                                        double avg = 0;
                                        bool shorted = false;
                                        for (int i = 0; i < tu.Threads; i++)
                                        {
                                            avg += tr[i].CPUUsageCounter;
                                            if (tr[i].CPUShortTime)
                                                shorted = true;
                                        }
                                        if (!shorted)
                                        {
                                            var avgZ = (long) (avg / tu.Threads);
                                            tu.TestResult.CPUUsage = avgZ;
                                        }
                                        else
                                        {
                                            tu.TestResult.CPUUsage = -1;
                                        }
                                    }
                                    break;
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        var failedResult = new FailedResult(tu, e);
                        if (e.InnerException != null && e.InnerException is AssertionException)
                        {
                            tu.WarningMessage = LanguageResource.AssertionFailed;
                            failedResult.IsAssertionFailed = true;
                            testSuite.FailedResult.Add(failedResult);
                        }
                        else
                        {
                            tu.WarningMessage = LanguageResource.UnhandledException;
                            testSuite.FailedResult.Add(failedResult);
                        }
                        try
                        {
                            if (tf != null) RunMethod(tf.TearDownLocalObjectType, tf.TearDownLocalObjectMethod);
                        }
                        finally
                        {
                            OnTestUnitFailed(new TestFailedEventArgs(failedResult));
                        }
                    }
                }
                lock (this)
                {
                    CompletedTests.Add(tu);
                }

                OnTestUnitEnd(new TestUnitRunEventArgs(tu));
                RunningTestUnits.Remove(tu);
            }

            try
            {
                if (tf != null) RunMethod(tf.TearDownObjectType, tf.TearDownObjectMethod);
            }
                // ReSharper disable EmptyGeneralCatchClause
            catch (Exception)
                // ReSharper restore EmptyGeneralCatchClause
            {
            }

            TestsFinished = true;
            Running = false;
            OnTestSuiteEnd(new TestSuiteRunEventArgs(testSuite));
            TotalTime = DateTime.Now.Subtract(_startTime);

            if (resetEvent != null) resetEvent.Set();
        }

        /// <summary>
        /// Queries the completed test units to get their results.
        /// </summary>
        public void QueryCompletedTests()
        {
            if (ResultWriter == null)
                throw new Exception("No Writer Defined.");
            foreach (TestUnit test in CompletedTests)
            {
                ResultWriter.WriteLine();
                ResultWriter.WriteLine("------------ {0} ------------", test.Name);
                ResultWriter.WriteLine(test.TestDescription);
                ResultWriter.WriteLine();
                if (test.IsIgnored)
                {
                    ResultWriter.WriteLine(LanguageResource.IgnoreReason, test.Name, test.IgnoredMessage);
                    continue;
                }
                if (!string.IsNullOrEmpty(test.WarningMessage))
                {
                    foreach (FailedResult result in test.TestSuite.FailedResult)
                    {
                        if (result.FailedUnit.Equals(test))
                        {
                            if (result.IsAssertionFailed)
                            {
                                ResultWriter.WriteLine(LanguageResource.WriterAssertionFailed, result.Message);
                            }
                            else
                            {
                                ResultWriter.WriteLine(LanguageResource.QueryCompletedTestsException,
                                                       test.Name, result.ExceptionType, result.Message,
                                                       result.FullMessage, result.Source);
                            }
                            break;
                        }
                    }
                    break;
                }

                foreach (TestType testType in test.TestTypes)
                {
                    switch (testType)
                    {
                        case TestType.MemoryUsage:
                            ResultWriter.WriteLine(LanguageResource.MemoryUsage, test.TestResult.MemoryUsage,
                                                   test.TestResult.MemoryUsageMb);
                            break;
                        case TestType.HitCount:
                            ResultWriter.WriteLine(LanguageResource.HitCount, test.TestResult.HitCount,
                                                   test.TestResult.HitCountTimeUnit);
                            break;
                        case TestType.CPUUsage:
                            if (test.TestResult.CPUUsage == -1)
                                ResultWriter.WriteLine(LanguageResource.ExecutionTooShort);
                            else
                                ResultWriter.WriteLine(LanguageResource.CPUUsage, test.TestResult.CPUUsage);
                            break;
                        case TestType.Duration:
                            ResultWriter.WriteLine(LanguageResource.Duration, test.TestResult.Duration,
                                                   test.TestResult.DurationTimePeriod);
                            break;
                        case TestType.None:
                            if (test.IsGenericCounterUnit)
                            {
                                if (test.TestResult.CounterValue == -1 && test.TestResult.IsGenericCounterTest)
                                    ResultWriter.WriteLine(LanguageResource.PCExecutionTooShort);
                                else
                                    ResultWriter.WriteLine(LanguageResource.PerformanceCounterResult,
                                                           test.TestResult.CategoryName,
                                                           test.TestResult.CounterName,
                                                           test.TestResult.InstanceName,
                                                           test.TestResult.CounterValue);
                            }
                            else
                            {
                                ResultWriter.WriteLine(LanguageResource.AssertionSuccessful);
                            }
                            break;
                    }
                }
            }
        }

        internal static void NormalizeTime(ref double duration, out TimePeriod timePeriod)
        {
            if (duration < 1000.0)
            {
                timePeriod = TimePeriod.Nanosecond;
            }
            else if (duration < 1000000.0)
            {
                duration = duration / 1000.0;
                timePeriod = TimePeriod.Microsecond;
            }
            else if (duration < 1000000000.0)
            {
                duration = duration / 1000000.0;
                timePeriod = TimePeriod.Millisecond;
            }
            else if (duration < 60000000000.0)
            {
                duration = duration / 1000000000.0;
                timePeriod = TimePeriod.Second;
            }
            else if (duration < 3600000000000.0)
            {
                duration = duration / 60000000000.0;
                timePeriod = TimePeriod.Minute;
            }
            else
            {
                duration = duration / 3600000000000.0;
                timePeriod = TimePeriod.Hour;
            }
        }

        internal static void NormalizeHitcount(ref double hitcount, TimePeriod timePeriod)
        {
            if (timePeriod == TimePeriod.Nanosecond)
                hitcount = 1.0 / hitcount;
            else if (timePeriod == TimePeriod.Microsecond)
                hitcount = 1000.0 / hitcount;
            else if (timePeriod == TimePeriod.Millisecond)
                hitcount = 1000000.0 / hitcount;
            else if (timePeriod == TimePeriod.Second)
                hitcount = 1000000000.0 / hitcount;
            else if (timePeriod == TimePeriod.Minute)
                hitcount = 60000000000.0 / hitcount;
            else if (timePeriod == TimePeriod.Hour)
                hitcount = 3600000000000.0 / hitcount;
        }
    }
}